// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/web_applications/app_browser_controller.h"

#include "base/bind.h"
#include "base/feature_list.h"
#include "base/strings/string_piece.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ssl/security_state_tab_helper.h"
#include "chrome/browser/themes/browser_theme_pack.h"
#include "chrome/browser/themes/theme_service.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window_state.h"
#include "chrome/browser/ui/tabs/tab_menu_model_factory.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/system_web_apps/system_web_app_delegate.h"
#include "chrome/browser/web_applications/system_web_apps/system_web_app_types.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/themes/autogenerated_theme_util.h"
#include "chrome/grit/generated_resources.h"
#include "components/security_state/core/security_state.h"
#include "components/url_formatter/url_formatter.h"
#include "components/webapps/browser/installable/installable_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "extensions/common/constants.h"
#include "net/base/escape.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/models/image_model.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/resize_utils.h"
#include "ui/gfx/image/image_skia.h"
#include "url/gurl.h"
#include "url/origin.h"

#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "chrome/browser/apps/icon_standardizer.h"
#endif

namespace {

SkColor GetAltColor(SkColor color) {
  return color_utils::BlendForMinContrast(
             color, color, absl::nullopt,
             kAutogeneratedThemeActiveTabPreferredContrast)
      .color;
}

}  // namespace

namespace web_app {

// static
bool AppBrowserController::IsWebApp(const Browser* browser) {
  return browser && browser->app_controller();
}

// static
bool AppBrowserController::IsForWebApp(const Browser* browser,
                                       const AppId& app_id) {
  return IsWebApp(browser) && browser->app_controller()->app_id() == app_id;
}

// static
std::u16string AppBrowserController::FormatUrlOrigin(
    const GURL& url,
    url_formatter::FormatUrlTypes format_types) {
  auto origin = url::Origin::Create(url);
  return url_formatter::FormatUrl(origin.opaque() ? url : origin.GetURL(),
                                  format_types, net::UnescapeRule::SPACES,
                                  nullptr, nullptr, nullptr);
}

const ui::ThemeProvider* AppBrowserController::GetThemeProvider() const {
  return theme_provider_.get();
}

AppBrowserController::AppBrowserController(
    Browser* browser,
    AppId app_id,
    bool has_tab_strip)
    : content::WebContentsObserver(nullptr),
      browser_(browser),
      app_id_(std::move(app_id)),
      has_tab_strip_(has_tab_strip),
      theme_provider_(
          ThemeService::CreateBoundThemeProvider(browser_->profile(), this)) {
  browser->tab_strip_model()->AddObserver(this);
}

AppBrowserController::AppBrowserController(Browser* browser, AppId app_id)
    : AppBrowserController(browser, std::move(app_id), false) {}

void AppBrowserController::Init() {
  UpdateThemePack();
}

AppBrowserController::~AppBrowserController() {
  browser()->tab_strip_model()->RemoveObserver(this);
}

bool AppBrowserController::ShouldShowCustomTabBar() const {
  if (!IsInstalled())
    return false;

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  if (!web_contents)
    return false;

  GURL start_url = GetAppStartUrl();
  base::StringPiece start_url_scheme = start_url.scheme_piece();

  bool is_internal_start_url_scheme =
      start_url_scheme == extensions::kExtensionScheme ||
      start_url_scheme == content::kChromeUIScheme ||
      start_url_scheme == content::kChromeUIUntrustedScheme;

  auto should_show_toolbar_for_url = [&](const GURL& url) -> bool {
    // If the url is unset, it doesn't give a signal as to whether the toolbar
    // should be shown or not. In lieu of more information, do not show the
    // toolbar.
    if (url.is_empty())
      return false;

    // Show toolbar when not using 'https', unless this is an internal app,
    // or origin is secure (e.g. localhost).
    if (!is_internal_start_url_scheme && !url.SchemeIs(url::kHttpsScheme) &&
        !webapps::InstallableManager::IsOriginConsideredSecure(url)) {
      return true;
    }

    // Page URLs that are not within scope
    // (https://www.w3.org/TR/appmanifest/#dfn-within-scope) of the app
    // corresponding to |start_url| show the toolbar.
    return !IsUrlInAppScope(url);
  };

  GURL visible_url = web_contents->GetVisibleURL();
  GURL last_committed_url = web_contents->GetLastCommittedURL();

  if (last_committed_url.is_empty() && visible_url.is_empty())
    return should_show_toolbar_for_url(initial_url());

  if (should_show_toolbar_for_url(visible_url) ||
      should_show_toolbar_for_url(last_committed_url)) {
    return true;
  }

  // Insecure external web sites show the toolbar.
  // Note: IsContentSecure is false until a navigation is committed.
  if (!last_committed_url.is_empty() && !is_internal_start_url_scheme &&
      !webapps::InstallableManager::IsContentSecure(web_contents)) {
    return true;
  }

  return false;
}

bool AppBrowserController::has_tab_strip() const {
  return has_tab_strip_;
}

bool AppBrowserController::HasTitlebarMenuButton() const {
  // Hide for system apps.
  return !system_app();
}

bool AppBrowserController::HasTitlebarAppOriginText() const {
  // Do not show origin text for System Apps.
  bool hide = system_app() ||
              base::FeatureList::IsEnabled(features::kHideWebAppOriginText);
  return !hide;
}

bool AppBrowserController::HasTitlebarContentSettings() const {
  // Do not show content settings for System Apps.
  return !system_app();
}

std::vector<PageActionIconType> AppBrowserController::GetTitleBarPageActions()
    const {
  if (system_app()) {
    return {PageActionIconType::kFind, PageActionIconType::kZoom};
  }

  std::vector<PageActionIconType> types_enabled;
  types_enabled.push_back(PageActionIconType::kFind);
  types_enabled.push_back(PageActionIconType::kManagePasswords);
  types_enabled.push_back(PageActionIconType::kTranslate);
  types_enabled.push_back(PageActionIconType::kZoom);
  types_enabled.push_back(PageActionIconType::kFileSystemAccess);
  types_enabled.push_back(PageActionIconType::kCookieControls);
  types_enabled.push_back(PageActionIconType::kLocalCardMigration);
  types_enabled.push_back(PageActionIconType::kSaveCard);

  return types_enabled;
}

bool AppBrowserController::IsInstalled() const {
  return false;
}

std::unique_ptr<TabMenuModelFactory>
AppBrowserController::GetTabMenuModelFactory() const {
  return nullptr;
}

bool AppBrowserController::AppUsesWindowControlsOverlay() const {
  return false;
}

bool AppBrowserController::IsWindowControlsOverlayEnabled() const {
  return false;
}

void AppBrowserController::ToggleWindowControlsOverlayEnabled() {}

gfx::Rect AppBrowserController::GetDefaultBounds() const {
  return gfx::Rect();
}

bool AppBrowserController::HasReloadButton() const {
  return true;
}

const SystemWebAppDelegate* AppBrowserController::system_app() const {
  return nullptr;
}

std::u16string AppBrowserController::GetLaunchFlashText() const {
  return GetFormattedUrlOrigin();
}

bool AppBrowserController::IsHostedApp() const {
  return false;
}

WebAppBrowserController* AppBrowserController::AsWebAppBrowserController() {
  return nullptr;
}

bool AppBrowserController::CanUserUninstall() const {
  return false;
}

void AppBrowserController::Uninstall(
    webapps::WebappUninstallSource webapp_uninstall_source) {
  NOTREACHED();
  return;
}

void AppBrowserController::UpdateCustomTabBarVisibility(bool animate) const {
  browser()->window()->UpdateCustomTabBarVisibility(ShouldShowCustomTabBar(),
                                                    animate);
}

void AppBrowserController::DidStartNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!initial_url().is_empty())
    return;
  // TODO(https://crbug.com/1218946): With MPArch there may be multiple main
  // frames. This caller was converted automatically to the primary main frame
  // to preserve its semantics. Follow up to confirm correctness.
  if (!navigation_handle->IsInPrimaryMainFrame())
    return;
  if (navigation_handle->GetURL().is_empty())
    return;
  SetInitialURL(navigation_handle->GetURL());
}

void AppBrowserController::DidFinishNavigation(
    content::NavigationHandle* navigation_handle) {
  if (!navigation_handle->IsInMainFrame() ||
      navigation_handle->IsSameDocument())
    return;

  // Reset the draggable regions so they are not cached on navigation.
  draggable_region_ = absl::nullopt;
}

void AppBrowserController::DOMContentLoaded(
    content::RenderFrameHost* render_frame_host) {
  // We hold off changing theme color for a new tab until the page is loaded.
  UpdateThemePack();
}

void AppBrowserController::DidChangeThemeColor() {
  UpdateThemePack();
}

void AppBrowserController::OnBackgroundColorChanged() {
  UpdateThemePack();
}

absl::optional<SkColor> AppBrowserController::GetThemeColor() const {
  absl::optional<SkColor> result;
  // HTML meta theme-color tag overrides manifest theme_color, see spec:
  // https://www.w3.org/TR/appmanifest/#theme_color-member
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  if (web_contents) {
    absl::optional<SkColor> color = web_contents->GetThemeColor();
    if (color)
      result = color;
  }

  if (!result)
    return absl::nullopt;

  // The frame/tabstrip code expects an opaque color.
  return SkColorSetA(*result, SK_AlphaOPAQUE);
}

absl::optional<SkColor> AppBrowserController::GetBackgroundColor() const {
  absl::optional<SkColor> color;
  if (auto* web_contents = browser()->tab_strip_model()->GetActiveWebContents())
    color = web_contents->GetBackgroundColor();
  return color ? SkColorSetA(*color, SK_AlphaOPAQUE) : color;
}

std::u16string AppBrowserController::GetTitle() const {
  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();
  if (!web_contents)
    return std::u16string();

  content::NavigationEntry* entry =
      web_contents->GetController().GetVisibleEntry();
  return entry ? entry->GetTitle() : std::u16string();
}

std::string AppBrowserController::GetTitleForMediaControls() const {
  // Only return the app name if we're a System Web App.
  return system_app() ? base::UTF16ToUTF8(GetAppShortName()) : std::string();
}

void AppBrowserController::OnTabStripModelChanged(
    TabStripModel* tab_strip_model,
    const TabStripModelChange& change,
    const TabStripSelectionChange& selection) {
  if (selection.active_tab_changed()) {
    content::WebContentsObserver::Observe(selection.new_contents);
    // Update themes when we switch tabs, or create the first tab, but not
    // when we create 2nd or subsequent tabs. They should keep current theme
    // until page loads. See |DOMContentLoaded|.
    if (change.type() != TabStripModelChange::kInserted ||
        tab_strip_model->count() == 1) {
      UpdateThemePack();
    }
  }
  if (change.type() == TabStripModelChange::kInserted) {
    for (const auto& contents : change.GetInsert()->contents)
      OnTabInserted(contents.contents);
  } else if (change.type() == TabStripModelChange::kRemoved) {
    for (const auto& contents : change.GetRemove()->contents)
      OnTabRemoved(contents.contents);
    // WebContents should be null when the last tab is closed.
    DCHECK_EQ(web_contents() == nullptr, tab_strip_model->empty());
  }
  UpdateCustomTabBarVisibility(/*animate=*/false);
}

CustomThemeSupplier* AppBrowserController::GetThemeSupplier() const {
  return theme_pack_.get();
}

void AppBrowserController::OnReceivedInitialURL() {
  UpdateCustomTabBarVisibility(/*animate=*/false);

  // If the window bounds have not been overridden, there is no need to resize
  // the window.
  if (!browser()->bounds_overridden())
    return;

  // The saved bounds will only be wrong if they are content bounds.
  if (!chrome::SavedBoundsAreContentBounds(browser()))
    return;

  // TODO(crbug.com/964825): Correctly set the window size at creation time.
  // This is currently not possible because the current url is not easily known
  // at popup construction time.
  browser()->window()->SetContentsSize(browser()->override_bounds().size());
}

void AppBrowserController::OnTabInserted(content::WebContents* contents) {
  if (!contents->GetVisibleURL().is_empty() && initial_url_.is_empty())
    SetInitialURL(contents->GetVisibleURL());
}

void AppBrowserController::OnTabRemoved(content::WebContents* contents) {}

ui::ImageModel AppBrowserController::GetFallbackAppIcon() const {
  gfx::ImageSkia page_icon = browser()->GetCurrentPageIcon().AsImageSkia();
  if (!page_icon.isNull()) {
#if BUILDFLAG(IS_CHROMEOS_ASH)
    return ui::ImageModel::FromImageSkia(
        apps::CreateStandardIconImage(page_icon));
#else
    return ui::ImageModel::FromImageSkia(page_icon);
#endif
  }

  // The icon may be loading still. Return a transparent icon rather
  // than using a placeholder to avoid flickering.
  SkBitmap bitmap;
  bitmap.allocN32Pixels(gfx::kFaviconSize, gfx::kFaviconSize);
  bitmap.eraseColor(SK_ColorTRANSPARENT);
  return ui::ImageModel::FromImageSkia(
      gfx::ImageSkia::CreateFrom1xBitmap(bitmap));
}

void AppBrowserController::UpdateDraggableRegion(const SkRegion& region) {
  draggable_region_ = region;

  if (on_draggable_region_set_for_testing_)
    std::move(on_draggable_region_set_for_testing_).Run();
}

void AppBrowserController::SetOnUpdateDraggableRegionForTesting(
    base::OnceClosure done) {
  on_draggable_region_set_for_testing_ = std::move(done);
}

void AppBrowserController::SetInitialURL(const GURL& initial_url) {
  DCHECK(initial_url_.is_empty());
  initial_url_ = initial_url;

  OnReceivedInitialURL();
}

void AppBrowserController::UpdateThemePack() {
  absl::optional<SkColor> theme_color = GetThemeColor();

  AutogeneratedThemeColors colors;
  // TODO(crbug.com/1053823): Add tests for theme properties being set in this
  // branch.
  absl::optional<SkColor> background_color = GetBackgroundColor();
  if (theme_color == last_theme_color_ &&
      background_color == last_background_color_) {
    return;
  }
  last_theme_color_ = theme_color;
  last_background_color_ = background_color;

  bool no_custom_colors = !theme_color && !background_color;
  bool non_tabbed_no_frame_color = !has_tab_strip_ && !theme_color;
  if (no_custom_colors || non_tabbed_no_frame_color) {
    theme_pack_ = nullptr;
    if (browser_->window()) {
      browser_->window()->UserChangedTheme(
          BrowserThemeChangeType::kWebAppTheme);
    }
    return;
  }

  if (!theme_color)
    theme_color = GetAltColor(*background_color);
  else if (!background_color)
    background_color = GetAltColor(*theme_color);

  // For regular web apps, frame gets theme color and active tab gets
  // background color.
  colors.frame_color = *theme_color;
  colors.active_tab_color = *background_color;
  colors.ntp_color = *background_color;

  colors.frame_text_color =
      color_utils::GetColorWithMaxContrast(colors.frame_color);
  colors.active_tab_text_color =
      color_utils::GetColorWithMaxContrast(colors.active_tab_color);

  theme_pack_ = base::MakeRefCounted<BrowserThemePack>(
      CustomThemeSupplier::AUTOGENERATED);
  BrowserThemePack::BuildFromColors(colors, theme_pack_.get());
  if (browser_->window())
    browser_->window()->UserChangedTheme(BrowserThemeChangeType::kWebAppTheme);
}

}  // namespace web_app
